/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using VSLangProj80;
using System.Threading;
using VSLangProj;
using System.IO;
using System.Windows.Forms;
using System.Diagnostics;
using System.ComponentModel;
using EnvDTE;
using System.Collections.ObjectModel;
using System.Xml;
using DaveSexton.DocProject.Resources;

namespace DaveSexton.DocProject.Engine
{
	/// <summary>
	/// Base type for classes that build documentation for a DocProject using a specific engine or external utility.
	/// </summary>
	/// <remarks>
	/// This class is not thread-safe.
	/// </remarks>
	public abstract class BuildEngine<TSettings, TOptions> : Component, IBuildEngine
		where TSettings : BuildSettings
		where TOptions : DocProjectOptions
	{
		#region Public Properties
		/// <summary>
		/// Gets the settings for the <see cref="Build" /> process.  The type of the settings are specified by derived classes using the 
		/// <typeparamref name="TSettings" /> argument.
		/// </summary>
		public virtual TSettings Settings
		{
			get
			{
				if (settings == null)
					settings = (TSettings) project.Provider.CreateBuildSettings(this);

				return settings;
			}
		}

		/// <summary>
		/// Gets the <see cref="DocProjectOptions" /> for the project specified when the <see cref="BuildSettings" /> instance was constructed.  The
		/// type of the settings are specified by derived classes using the <typeparamref name="TOptions" /> argument.
		/// </summary>
		public virtual TOptions Options
		{
			get
			{
				if (options == null)
					options = (TOptions) project.Options;

				return options;
			}
		}

		/// <summary>
		/// Gets the <see cref="IDocProject" /> instance for which the engine will <see cref="Build" /> compiled help.
		/// </summary>
		public IDocProject Project
		{
			get
			{
				return project;
			}
		}

		/// <summary>
		/// Gets the collection of steps for the <see cref="Build" /> process.
		/// </summary>
		public BuildStepCollection Steps
		{
			get
			{
				if (steps == null)
				{
					steps = CreateSteps();

					if (steps == null)
						steps = new BuildStepCollection(this);
				}

				return steps;
			}
		}

		public DocProjectItemCollection BuildItems
		{
			get
			{
				DocProjectItemCollection items = BuildItemsInternal;

				if (project.IsDocSite)
				{
					DocProjectFile contents = new DocProjectFile(Settings.DocSiteContentsXmlFile, true, true,
						// these values depend on the HelpSystem implementation and may be overriden by derived types (default is full / none)
						DocSiteContentsMissingBuildType, DocSiteContentsUpdatedBuildType);

					DocProjectFile index = new DocProjectFile(Settings.DocSiteIndexXmlFile, true, true,
						// these values depend on the HelpSystem implementation and may be overriden by derived types (default is full / none)
						DocSiteIndexMissingBuildType, DocSiteIndexUpdatedBuildType);

					items.Add(contents);
					items.Add(index);
				}

				return items;
			}
		}

		public virtual DateTime LastBuild
		{
			get
			{
				DateTime? lastBuild = null;

				foreach (string file in Settings.Targets)
				{
					if (!string.IsNullOrEmpty(file))
					{
						DateTime writeTime = File.GetLastWriteTime(file);

						if (lastBuild == null || writeTime < lastBuild)
							lastBuild = writeTime;
					}
				}

				return (lastBuild.HasValue) ? lastBuild.Value : DateTime.MinValue;
			}
		}

		public bool HasSourcesChanged
		{
			get
			{
				return HasChangedSince(LastBuild, project.AllSources);
			}
		}

		/// <summary>
		/// Gets an implementation of the <see cref="HelpSystem" /> class, which provides information about the table of contents and index.  The build engine
		/// uses this information to create the supporting files for DocSite projects.
		/// </summary>
		public abstract HelpSystem HelpSystem { get; }

		public BuildContext CurrentContext
		{
			get
			{
				return currentContext;
			}
		}

		public BuildType BuildType
		{
			get
			{
				return buildType;
			}
			set
			{
				if (!Enum.IsDefined(typeof(BuildType), value))
					throw new System.ComponentModel.InvalidEnumArgumentException("value", (int) value, typeof(BuildType));

				buildType = value;
			}
		}

		public BuildTrace TraceHandler
		{
			get
			{
				return trace;
			}
			set
			{
				trace = value;
			}
		}

		public virtual MissingBuildTypeDependency DocSiteContentsMissingBuildType
		{
			get
			{
				return MissingBuildTypeDependency.Full;
			}
		}

		public virtual UpdatedBuildTypeDependency DocSiteContentsUpdatedBuildType
		{
			get
			{
				return UpdatedBuildTypeDependency.None;
			}
		}

		public virtual MissingBuildTypeDependency DocSiteIndexMissingBuildType
		{
			get
			{
				return MissingBuildTypeDependency.Full;
			}
		}

		public virtual UpdatedBuildTypeDependency DocSiteIndexUpdatedBuildType
		{
			get
			{
				return UpdatedBuildTypeDependency.None;
			}
		}

		public virtual Type BuildProcessComponentInitializerType
		{
			get
			{
				return null;
			}
		}
		#endregion

		#region Private / Protected
		protected abstract DocProjectItemCollection BuildItemsInternal { get; }

		/// <summary>
		/// This field supports lazy initialization via the <see cref="Settings" /> property.
		/// </summary>
		private TSettings settings;
		/// <summary>
		/// This field supports lazy initialization via the <see cref="Options" /> property.
		/// </summary>
		private TOptions options;
		/// <summary>
		/// This field supports lazy initialization via the <see cref="CreateSteps" /> method.
		/// </summary>
		private BuildStepCollection steps;

		private BuildTrace trace;
		private readonly IDocProject project;
		private readonly object buildSync = new object();
		private BuildType buildType = BuildType.Default;
		private BuildContext currentContext;
		private Exception lastException;	// nulled before Build method returns to the caller
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="BuildEngine" /> class for derived classes.
		/// </summary>
		/// <param name="project">DocProject or DocSite for which the engine will build compiled help.</param>
		protected BuildEngine(IDocProject project)
		{
			this.project = project;
		}
		#endregion

		#region Methods
		protected abstract BuildStepCollection CreateSteps();

		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		public void SetBuildTypeWithPrecedence(BuildType type)
		{
			BuildType = GetBuildTypeWithPrecedence(type, BuildType);
		}

		private static BuildType GetBuildTypeWithPrecedence(BuildType type1, BuildType type2)
		{
			if ((int) type1 > (int) type2)
				return type1;
			else
				return type2;
		}

		protected virtual bool HasChangedSince(DateTime dateTime, SourceCollection sources)
		{
			foreach (Source source in sources)
			{
				if (source.HasChangedSince(dateTime, project.Provider.AllSupportedExternalSourceFileExtensions))
				{
					System.Diagnostics.Debug.WriteLine(source, "Source changed since " + dateTime.ToString());
					return true;
				}
			}

			return false;
		}

		private void ClearSteps()
		{
			if (steps == null)
				return;

			foreach (IBuildStep step in steps)
			{
				try
				{
					step.Dispose();
				}
				// Dynamic build steps can be created in the Build Process Component, which has already been unloaded.
				catch (AppDomainUnloadedException) { }
			}

			// Setting the field to null will cause the Steps property to recreate the steps the next time it's accessed.
			steps = null;
		}

		public bool Build()
		{
			DocProjectEnvironment.ShowTrace();

			BuildContext context = new BuildContext(this, trace ?? new VSEnvironmentBuildTrace(Project.Name));

			try
			{
				Build(context);
			}
			finally
			{
				VSEnvironmentHost.ResetProgress(true);
			}

			return context.BuildState == BuildState.Successful;
		}

		/// <summary>
		/// Performs the build sequence for the <see cref="Project" /> using the current <see cref="Settings" />.
		/// </summary>
		/// <remarks>
		/// This method is thread-safe.
		/// </remarks>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public virtual void Build(BuildContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			if (context.Engine != this)
				throw new ArgumentException(Errors.InvalidBuildContextEngine, "context");

			lock (buildSync)
			{
				context.WaitHandle.Reset();
				context.SetProgress("Initializing help build...", 5, 100);
				context.CurrentDirectory = Settings.TargetDirectory;

				currentContext = context;

				try
				{
					context.Sources = project.AllSources;
					context.BuildItems = BuildItems;

					// NOTE: Saving options may cause the build type to change (see the Save method), so this call must occur before the build type is read
					if (options != null)
						options.Save();

					// NOTE: This must be called after project options are saved in case the BuildItemsInternal implementation depends on project options.
					Refresh();

					// NOTE: A Full build is assigned to the BuildType property by the build engine when the DocProjectOptions.ProcessComponentTypeName property has changed.
					// Although a full build takes precedence the build items must still be evaluated in case of an error condition (they are evaluated by calling CanBuild).
					context.BuildType = GetBuildTypeWithPrecedence(context.BuildType, BuildType);

					// NOTE: The Steps collection uses lazy initialization.  It's created when CanBuild is called and after the build has finished it's reset to null.
					// The purpose is that the steps collectiong might not need to be created at all if, for one reason or another, the engine cannot start the build.

					if (!CanBuild(context))
					{
						if (context.BuildState != BuildState.Failed)
							// Although the build was not successful, it didn't fail, 
							// so assume that it is just being skipped - in which case
							// MSBuild should not fail either...
							context.BuildState = BuildState.Successful;

						return;
					}

					AppDomain bpcDomain = null;		// Build Process Component domain

					BuildStepExecutionEngine stepEngine = new BuildStepExecutionEngine(context, ExecuteStep);
					context.stepEngine = stepEngine;

					try
					{
						context.SetProgress("Starting help build...", 10, 100);

						if (StartBuild(context, out bpcDomain) && !context.Canceled)
							BuildInternal(context);
					}
					catch (Exception ex)
					{
						lastException = ex;
						context.BuildState = BuildState.Failed;

						context.TraceLine();
						context.TraceLine(ex.GetType().FullName + " was thrown during the build process: ");
						context.TraceLine();
						context.TraceLine(ex.Message);
						context.TraceLine();

						Log.Exception(ex, "Event: Build process running.");
					}
					finally
					{
						try
						{
							try
							{
								EndBuild(context);

								if (context.BuildState == BuildState.Successful)
									context.SetStatus("Help build succeeded");
								else
								{
									context.SetStatus("Help build " + ((context.Canceled) ? "canceled" : "failed"));

									if (lastException != null)
										context.AddErrorToList(lastException.Message);
								}
							}
							finally
							{
								try
								{
									if (bpcDomain != null)
										AppDomain.Unload(bpcDomain);
								}
								catch (AppDomainUnloadedException) { }
								catch (Exception ex)
								{
									Log.Exception(ex, Errors.BPCUnloadAppDomainError);

									context.AddErrorToList(Errors.BPCUnloadAppDomainError + " " + ex.Message);
								}

								DisplayResults(context);
							}
						}
						finally
						{
							context.FlushItemsToList();
						}
					}
				}
				finally
				{
					try
					{
						currentContext = null;
						lastException = null;

						if (context.Canceled || context.BuildState == BuildState.Failed)
							BuildType = context.BuildType;
						else
							BuildType = BuildType.Default;

						// The steps must be recreated each time the engine performs a build because they may 
						// have been created using settings that have since changed.
						ClearSteps();
					}
					finally
					{
						context.WaitHandle.Set();
					}
				}
			}
		}

		protected virtual bool HasSources(BuildContext context)
		{
			return context.Sources.Count > 0;
		}

		private bool CanBuild(BuildContext context)
		{
			if (!HasSources(context))
			{
				context.TraceLine();
				context.Trace("Warning: ");
				context.AddWarningToList(context.ProjectName, "There are no project references or sources for documentation.", true);
				context.TraceLine();
				context.TraceLine("Help build skipped.");
				context.TraceLine();

				return false;
			}

			// NOTE: Lazy initialization is used to generate the build steps collection.  The steps will be created here since the 
			// Steps property won't be read until now.  The steps field is assigned to null again after the build completes.
			if (Steps.Count == 0)
			{
				context.TraceLine();
				context.Trace("Warning: ");
				context.AddWarningToList(context.ProjectName, "There are no steps to execute.", true);
				context.TraceLine();
				context.TraceLine("Help build skipped.");
				context.TraceLine();

				return false;
			}

			return CanBuildByTypeAndItems(context);
		}

		private bool CanBuildByTypeAndItems(BuildContext context)
		{
			bool forcedSkip;

			if (!EvaluateBuildType(context, LastBuild, out forcedSkip))
			// one or more files or folders with a MissingBuildTypeDependency of Error are missing
			{
				context.BuildState = BuildState.Failed;

				context.TraceLine();
				context.TraceLine("Help build failed because one or more of the required files or folders is missing.");
				context.TraceLine();
				return false;
			}

			// NOTE: After EvaluateBuildType, context.BuildType will never be "Default" (even if it returned false).

			if (context.BuildType != BuildType.Skip)
			// allow derived classes to validate the build type
			{
				BuildType calculatedType = context.BuildType;

				context.BuildType = ValidateBuildType(context, context.BuildType);
				forcedSkip = true;	// set just in case type returns as Skip

				if (context.BuildType == BuildType.Default)
					context.BuildType = calculatedType;
			}

			if (context.BuildType == BuildType.Skip)
			{
				context.TraceLine();

				if (forcedSkip)
					context.TraceLine("Help build skipped.");
				else
					context.TraceLine("Help build skipped because the output is up to date with respect to the build items and sources.");

				context.TraceLine();
				return false;
			}

			return true;
		}

		private bool EvaluateBuildType(BuildContext context, DateTime lastBuild, out bool forcedSkip)
		{
			if (context.BuildType == BuildType.Skip)
			{
				forcedSkip = true;
				return false;
			}

			forcedSkip = false;
			bool error = false;

			BuildType evaluatedType = EvaluateBuildTypeFromBuildItems(context, lastBuild, out error);

			if (!error && context.BuildType == BuildType.Default)
			{
				if (evaluatedType == BuildType.Full || HasChangedSince(lastBuild, context.Sources))
					context.BuildType = BuildType.Full;
				else
					context.BuildType = evaluatedType;
			}

			return !error;
		}

		private static BuildType EvaluateBuildTypeFromBuildItems(BuildContext context, DateTime lastBuild, out bool error)
		{
			error = false;
			BuildType type = BuildType.Skip;

			foreach (DocProjectItemBase item in context.BuildItems)
			{
				EvaluateForMissingBuildTypeDependency(context, item, ref type, ref error);

				if (error)
					// there's no need to check for updated items since the build will fail anyway
					continue;

				EvaluateForUpdatedBuildTypeDependency(item, lastBuild, ref type);
			}

			return type;
		}

		private static void EvaluateForMissingBuildTypeDependency(BuildContext context, DocProjectItemBase item, ref BuildType type, ref bool error)
		{
			switch (item.MissingBuildTypeDependency)
			{
				case MissingBuildTypeDependency.Full:
					if (!error && type != BuildType.Full && !item.Exists)
						type = BuildType.Full;
					break;
				case MissingBuildTypeDependency.Partial:
					if (!error && type == BuildType.Skip && !item.Exists)
						type = BuildType.Partial;
					break;
				case MissingBuildTypeDependency.Error:
					if (!item.Exists)
					{
						context.TraceLine();

						DocProjectFileGroup group = item as DocProjectFileGroup;

						if (group == null)
							context.TraceLine("Missing required build item: " + item.Path.CompletePath);
						else
						{
							ICollection<string> missingFiles = group.MissingFiles;

							if (missingFiles.Count == 0)
								context.TraceLine("Missing one or more required build items in file group: " + item.Path.CompletePath);
							else
							{
								foreach (string file in missingFiles)
									context.TraceLine("Missing required build item in file group: " + file);
							}
						}

						error = true;
					}
					break;
			}
		}

		[System.Diagnostics.DebuggerHidden]
		private static void EvaluateForUpdatedBuildTypeDependency(DocProjectItemBase item, DateTime lastBuild, ref BuildType type)
		{
			try
			{
				switch (item.UpdatedBuildTypeDependency)
				{
					case UpdatedBuildTypeDependency.Full:
						if (type != BuildType.Full && item.HasChangedSince(lastBuild))
						{
							System.Diagnostics.Debug.WriteLine(item, "Build item changed since " + lastBuild.ToString());
							type = BuildType.Full;
						}
						break;
					case UpdatedBuildTypeDependency.Partial:
						if (type == BuildType.Skip && item.HasChangedSince(lastBuild))
						{
							System.Diagnostics.Debug.WriteLine(item, "Partial build item changed since " + lastBuild.ToString());
							type = BuildType.Partial;
						}
						break;
				}
			}
			catch (FileNotFoundException) { }
			catch (DirectoryNotFoundException) { }
		}

		protected virtual BuildType ValidateBuildType(BuildContext context, BuildType type)
		{
			return type;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private bool StartBuild(BuildContext context, out AppDomain domain)
		{
			domain = null;

			context.TraceLine();
			context.TraceLine("Starting {0}help build for {1}...",
				(context.BuildType == BuildType.Partial) ? "partial " : string.Empty,
				context.ProjectName);

			BuildProcessComponentProxy processComponent = null;

			try
			{
				OnBuildStarting(new BuildEventArgs(context));

				if (context.Canceled)
					return false;

				processComponent = CreateBuildProcessComponent(context, out domain);

				if (processComponent != null)
				// optional component - null is returned if one isn't configured
				{
					try
					{
						processComponent.BuildStarting(context);
					}
					catch (Exception ex)
					{
						throw new BuildProcessComponentException(processComponent, ex);
					}

					if (context.Canceled)
						return false;
				}
			}
			catch (Exception ex)
			{
				lastException = ex;
				context.BuildState = BuildState.Failed;

				context.TraceLine();
				context.TraceLine(ex.GetType().FullName + " was thrown while the build process was starting: ");
				context.TraceLine();
				context.TraceLine(ex.Message);

				Log.Exception(ex, "Event: Build Process Starting");

				return false;
			}

			// true indicates to the caller that the build can continue
			return true;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength")]
		private BuildProcessComponentProxy CreateBuildProcessComponent(BuildContext context, out AppDomain domain)
		{
			domain = null;

			string typeName = Options.ProcessComponentTypeName;

			if (typeName == null || (typeName = typeName.Trim()) == string.Empty)
				return null;

			bool fileSpecified = false, assemblySpecified = false;
			string file = null, basePath = null;
			int separatorIndex = typeName.LastIndexOf('|');

			if (separatorIndex > 0 && separatorIndex < typeName.Length - 4)
			{
				string path = typeName.Substring(separatorIndex + 1);

				if ((path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
					&& PathFormat.IsValid(path, false))
				{
					fileSpecified = true;
					file = Path.IsPathRooted(path) ? path : Path.Combine(project.Directory, PathFormat.MakeLocal(path));
					basePath = Path.GetDirectoryName(file);
					typeName = typeName.Substring(0, separatorIndex);
				}
			}

			if (file == null)
			{
				// An assembly, if specified, is separated from the type name by a single comma (,).
				// A full type name with an embedded comma must use a backslash (\) as an escape character according to MSDN: 
				// http://msdn2.microsoft.com/en-us/library/yfsftwz6.aspx
				// Therefore, match the first comma that is not directly preceeded by an odd number of backslashes 
				// (an even number will escape only the backslashes, but not the comma): 
				assemblySpecified = System.Text.RegularExpressions.Regex.IsMatch(typeName, @"((^|[^\\])(\\\\)+|(?<!\\)),.+",
					System.Text.RegularExpressions.RegexOptions.ExplicitCapture | System.Text.RegularExpressions.RegexOptions.Singleline);

				if (!assemblySpecified)
					file = project.Output.OutputFile.Path;

				basePath = project.Output.OutputPath;
			}

			if (!assemblySpecified)
			{
				if (string.IsNullOrEmpty(file) || !File.Exists(file))
					throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
						(fileSpecified) ? Errors.BPCAssemblyFileNotFound : Errors.BPCLocalAssemblyNotFound, file));
				else if (!file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !file.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
					throw new InvalidOperationException(Errors.BPCRequiresManagedAssembly);
			}

			context.processComponent = BuildProcessComponentProxy.CreateInstance(file, typeName, basePath, context, out domain);

			if (context.processComponent != null)
			{
				Type type = BuildProcessComponentInitializerType;

				if (type != null)
				{
					BuildProcessComponentInitializer initializer = BuildProcessComponentInitializer.Create(domain, type);

					if (initializer != null)
						initializer.Initialize(context, context.processComponent);
				}
			}

			return context.processComponent;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void BuildInternal(BuildContext context)
		{
			context.SetProgress("Preparing to build help...", 15, 100);

			try
			{
				PrepareForBuild(context);
			}
			catch (Exception ex)
			{
				lastException = ex;
				context.BuildState = BuildState.Failed;

				context.TraceLine();
				context.TraceLine(ex.GetType().FullName + " was thrown during build preparation: ");
				context.TraceLine();
				context.TraceLine(ex.Message);

				Log.Exception(ex, "Event: Build process preparation.");

				return;
			}

			if (context.Canceled)
				return;

			context.TraceLine("Building documentation for {0}...", context.ProjectName);

			context.BuildState = BuildState.Running;

			try
			{
				context.stepEngine.ExecuteAll();
			}
			catch (Exception ex)
			{
				lastException = ex;
				context.BuildState = BuildState.Failed;

				context.TraceLine();
				context.TraceLine(ex.GetType().FullName + " was thrown while executing steps: ");
				context.TraceLine();
				context.TraceLine(ex.Message);

				Log.Exception(ex, "Event: Build process step execution.");

				return;
			}

			if (context.Canceled || !context.SuccessfullyExecutedAllRequiredSteps)
				return;

			context.TraceLine();
			context.TraceLine("Build step execution complete.");

			if (context.IsDocSite && context.BuildState == BuildState.Running)
			{
				context.SetProgress("Building DocSite files...", 90, 100);

				context.TraceLine();
				context.TraceLine("Building DocSite files...");

				try
				{
					CreateDocSiteFiles(context);
				}
				catch (Exception ex)
				{
					lastException = ex;
					context.BuildState = BuildState.Failed;

					context.TraceLine();
					context.TraceLine(ex.GetType().FullName + " was thrown while building the DocSite files: ");
					context.TraceLine();
					context.TraceLine(ex.Message);
					context.TraceLine();

					Log.Exception(ex, "Event: Build DocSite files.");

					return;
				}
			}

			context.SetProgress("Verifying help output...", 100, 100);

			context.BuildState = BuildState.Verifying;

			context.TraceLine();
			context.TraceLine("Verifying output...");

			try
			{
				VerifyOutput(context);
				context.TraceLine("Done.");
			}
			catch (Exception ex)
			{
				lastException = ex;
				context.BuildState = BuildState.Failed;

				context.TraceLine();
				context.TraceLine(ex.GetType().FullName + " was thrown during output verification: ");
				context.TraceLine();
				context.TraceLine(ex.Message);
				context.TraceLine();
				context.TraceLine("Note: There may be problems with the output.");

				Log.Exception(ex, "Event: Build process output verification.");
			}
		}

		protected virtual void PrepareForBuild(BuildContext context)
		{
			context.CopyStepsFromEngine();
			context.FilterStepsForPartialBuild();

			EnsureOutputDirectoryStructure(context);

			foreach (KeyValuePair<SystemPath, IEnumerable<SystemPath>> source in Settings.IndexBySource(context))
			{
				SystemPath input = source.Key;

				try
				{
					PrepareSourceForBuild(input, new List<SystemPath>(source.Value).AsReadOnly(), context);
				}
				catch (IOException ex)
				{
					lastException = ex;
					context.TraceLine();
					context.TraceLine("Error preparing source assembly.");
					context.TraceLine(ex.GetType().FullName + ": " + ex.Message);
					context.TraceLine("Source: " + input.Path);

					Log.Exception(ex, "BuildEngine: Preparing source assembly: " + input.Path);
				}
			}
		}

		/// <param name="source">Full path to a source file or a folder that may contain one or more sources.</param>
		protected virtual void PrepareSourceForBuild(SystemPath source, IList<SystemPath> xmlDocumentationFiles, BuildContext context)
		{
			// do nothing (this method is for derived classes)
		}

		private void EnsureOutputDirectoryStructure(BuildContext context)
		{
			context.TraceLine("Preparing target directory...");

			string targetPath = Settings.TargetDirectory;

			if (!Directory.Exists(targetPath))
				Directory.CreateDirectory(targetPath);

			// The Sandcastle build engine, for example, deletes the entire Html folder in PrepareTargetDirectory (called next).
			// In cases where there are many HTML files this can take quite some time and the UI doesn't seem to catch up right away, 
			// so it appears to the user that the build hasn't even started yet.  DoEvents solves this problem.
			Application.DoEvents();

			PrepareTargetDirectory(context);
		}

		protected abstract void PrepareTargetDirectory(BuildContext context);

		/// <summary>
		/// Deletes the specified file if it exists.
		/// </summary>
		/// <param name="relativePath">Relative path from the project's root directory.</param>
		protected void RemoveTargetFile(string relativePath)
		{
			project.GetItem(relativePath).Delete();
		}

		/// <summary>
		/// Ensures that the specified directory exists and that it's empty.  If the directory exists it is deleted and recreated.
		/// </summary>
		/// <param name="relativePath">Relative path from the project's root directory.</param>
		protected void CleanTargetSubdirectory(string relativePath)
		{
			CleanProjectDirectory(Path.Combine(Settings.TargetDirectory, relativePath));
		}

		[DebuggerHidden]
		protected void DeleteTargetSubdirectory(string relativePath)
		{
			project.GetFolder(Path.Combine(Settings.TargetDirectory, relativePath)).Delete();
		}

		/// <summary>
		/// Ensures that the specified directory exists.
		/// </summary>
		/// <param name="relativePath">Relative path from the project's root directory.</param>
		protected void EnsureTargetSubdirectory(string relativePath)
		{
			string directory = Path.Combine(Settings.TargetDirectory, relativePath);

			if (!Directory.Exists(directory))
				// this method will not throw an exception if the directory already exists
				Directory.CreateDirectory(directory);
		}

		protected void EnsureProjectSubdirectory(string relativePath)
		{
			string directory = Path.Combine(Settings.ProjectDirectory, relativePath);

			if (!Directory.Exists(directory))
				// this method will not throw an exception if the directory already exists
				Directory.CreateDirectory(directory);
		}

		protected void CleanProjectSubdirectory(string relativePath)
		{
			CleanProjectDirectory(relativePath);
		}

		private void CleanProjectDirectory(string path)
		{
			bool isProjectItem = false;

			IProjectItem folder = project.GetFolder(path);

			if (folder.Exists)
			{
				isProjectItem = folder.IsVisible;
				folder.Delete();
			}

			if (isProjectItem)
				// NOTE: Show will automatically create the directory and include it as a project item.
				folder.Show();
			else
				// NOTE: CreateDirectory will not throw an exception if the directory already exists
				Directory.CreateDirectory(folder.Path);
		}

		[DebuggerHidden]
		protected void DeleteProjectSubdirectory(string relativePath)
		{
			project.GetFolder(relativePath).Delete();
		}

		protected virtual void ExecuteStep(IBuildStep step, BuildContext context)
		{
			if (step is InternalCreateDocSiteContentsStep || step is InternalCreateDocSiteIndexStep)
			{
				context.SetStatus(step.Name + "...");

				step.Execute(context);
			}
			else
			{
				string text = string.Format(System.Globalization.CultureInfo.CurrentCulture,
					"Executing step {0} of {1}: {2}", context.CurrentStepIndex + 1, context.Steps.Count, step.Name);

				context.SetStatus(text);

				step.Execute(context);

				// define the meter range that is reserved for total step progress
				const int min = 20, max = 85;

				context.SetProgress(text, context.PercentComplete * (max - min) / 100 + min, 100);
			}
		}

		protected virtual void VerifyOutput(BuildContext context)
		{
			bool tracedBlankLine = false;
			bool missing = false;
			StringBuilder missingFiles = new StringBuilder();

			foreach (DocProjectItemBase item in context.BuildItems)
			{
				if (item.IsRequiredOutput && !item.Exists)
				{
					if (!tracedBlankLine)
					{
						context.TraceLine();
						tracedBlankLine = true;
					}

					missingFiles.AppendLine(item.Path.CompletePath);
					missing = true;
				}
			}

			if (missing)
				throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Errors.MissingRequiredOutput, Environment.NewLine, missingFiles));
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void EndBuild(BuildContext context)
		{
			context.SetProgress("Completing help build...", 95, 100);

			try
			{
				// BuildState is exepected to be Verifying at this point. Otherwise, it may be Failed if there was a major error or something else
				// if it's set by a derived class (although it cannot be set by derived classes as of Version 1.1.0)
				if (context.BuildState != BuildState.Failed && context.SuccessfullyExecutedAllRequiredSteps)
					context.BuildState = BuildState.Successful;
				else if (context.BuildState != BuildState.Successful)
					context.BuildState = BuildState.Failed;

				OnBuildCompleted(new BuildEventArgs(context));

				if (context.processComponent != null)
				{
					try
					{
						context.processComponent.BuildCompleted(context);
					}
					catch (Exception ex)
					{
						throw new BuildProcessComponentException(context.processComponent, ex);
					}
				}
			}
			catch (Exception ex)
			{
				lastException = ex;
				context.BuildState = BuildState.Failed;

				context.TraceLine();
				context.TraceLine(ex.GetType().FullName + " was thrown finalizing the build process: ");
				context.TraceLine();
				context.TraceLine(ex.Message);
				context.TraceLine();

				Log.Exception(ex, "Event: Build process finalization.");
			}
		}

		protected virtual void CreateDocSiteFiles(BuildContext context)
		{
			HelpSystem help = HelpSystem;

			if (help == null)
				throw new InvalidOperationException(Errors.BuildEngineNoHelpSystem);

			context.TraceLine();

			using (InternalCreateDocSiteContentsStep createTOCStep = new InternalCreateDocSiteContentsStep(help, this))
			{
				using (InternalCreateDocSiteIndexStep createIndexStep = new InternalCreateDocSiteIndexStep(help, this))
				{
					context.ExecuteOutOfBand(createTOCStep, createIndexStep);
				}
			}
		}

		private static void DisplayResults(BuildContext context)
		{
			context.TraceLine();

			if (context.ExecutedStepsCount > 0)
			{
				context.TraceLine("Successful Steps: " + (context.SuccessfulStepsCount) + " of " + context.Steps.Count);
				context.TraceLine("Failed Steps: " + context.FailedStepsCount);
			}
			else
				context.TraceLine("No steps executed.");

			context.TraceLine();

			switch (context.BuildState)
			{
				case BuildState.Successful:
					context.SetStatus("Help build completed");

					context.TraceLine("{0} {1}help build complete{2}.", context.ProjectName,
						(context.BuildType == BuildType.Partial) ? "partial " : string.Empty,
						(context.FailedOptionalStepsCount > 0) ? "d with one or more failed steps" : null);
					break;
				default:
					if (context.Canceled)
					{
						// no status required since Visual Studio will automatically write Build Canceled
						context.TraceLine("{0} {1}help build canceled.", context.ProjectName,
							(context.BuildType == BuildType.Partial) ? "partial " : string.Empty);
					}
					else
					{
						context.TraceLine("{0} {1}help build failed.", context.ProjectName,
							(context.BuildType == BuildType.Partial) ? "partial " : string.Empty);
					}
					break;
			}

			context.TraceLine();
		}

		public bool Clean()
		{
			DocProjectEnvironment.ShowTrace();

			BuildContext context = new BuildContext(this, trace ?? new VSEnvironmentBuildTrace(Project.Name));

			Clean(context);

			return context.BuildState == BuildState.Successful;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public virtual void Clean(BuildContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			if (context.Engine != this)
				throw new ArgumentException(Errors.InvalidBuildContextEngine, "context");

			lock (buildSync)
			{
				context.WaitHandle.Reset();
				context.BuildItems = BuildItems;
				context.BuildState = BuildState.Running;

				try
				{
					CleanInternal(context);

					context.BuildState = BuildState.Successful;
				}
				catch (Exception ex)
				{
					context.BuildState = BuildState.Failed;

					context.AddErrorToList(ex.Message, false);

					context.TraceLine();
					context.TraceLine(ex.GetType().FullName + " was thrown while the project was being cleaned: ");
					context.TraceLine();
					context.TraceLine(ex.Message);
					context.TraceLine();

					Log.Exception(ex, "Event: Clean project.");
				}
				finally
				{
					try
					{
						context.FlushItemsToList();
					}
					finally
					{
						context.WaitHandle.Set();
					}
				}
			}
		}

		protected virtual void CleanInternal(BuildContext context)
		{
			DocProjectItemCollection items = context.BuildItems;

			if (items.Count == 0)
				return;

			foreach (DocProjectItemBase item in items)
			{
				if (item.RemoveOnClean && item.AnyExists)
				{
					context.TraceLine("Deleting item \"{0}\"", item);

					if (!item.Delete(project))
						context.TraceLine(Errors.ProjectItemNotRemoved, item.Path.CompletePath);
				}
			}
		}

		public virtual void Cancel()
		{
			if (currentContext != null)
				currentContext.Cancel();
		}

		public virtual void Refresh()
		{
			if (settings != null)
				settings.Refresh();
		}

		/// <remarks>
		/// This method used to be part of the <see cref="VerifyOutput"/> method; however, since DocProject now builds
		/// using an MSBuild task, using DTE.ProjectItems (or any ProjectItems collection) forwards calls to MSBuild to
		/// set properties directly, which throws an exception during the build process.  The IdeEventListener class, 
		/// instead of building the project, now just listens for the build to be completed so it can synchronize the 
		/// project items.  Unfortunately, this occurs after the build has completed and the BuildContext is no longer available.
		/// </remarks>
		public virtual bool IncludeBuildItems()
		{
			bool prompt = !Options.DoNotAskIncludeOutputInProject;
			bool include = Options.DefaultIncludeOutputInProject;
			bool includedAny = false;

			if (!prompt && !include)
				return false;

			IncludeOutputMessageBox dialog = (prompt) ? new IncludeOutputMessageBox() : null;

			try
			{
				if (prompt && dialog != null)
				{
					dialog.ProjectName = project.Name;
					dialog.ApplyToAllChecked = Options.DefaultApplyToAll;
				}

				foreach (DocProjectItemBase item in BuildItems)
				{
					if (!item.IncludeInProject || !item.AnyExists)
						continue;

					bool includedPath;
					bool continueToPrompt = SetOutputVisibility(item.Path, dialog, ref include, out includedPath);

					if (includedPath)
						includedAny = true;

					if (dialog != null && !continueToPrompt)
					// User checked "Apply to all" or "Don't ask me this again"
					{
						if (!include)
							// On the dialog the user clicked "No" to exclude the remaining output files from the project
							return includedAny;

						// User clicked "Yes" to include the remaining output files as project items

						// setting the dialog variable to null causes VerifyOutputVisibility to just include the file without prompting the user
						dialog.Dispose();
						dialog = null;
					}
				}
			}
			finally
			{
				if (dialog != null)
					dialog.Dispose();

				project.Refresh();
			}

			return includedAny;
		}

		private bool SetOutputVisibility(SystemPath path, IncludeOutputMessageBox dialog, ref bool include, out bool includedPath)
		{
			includedPath = false;
			bool continueToPrompt = (dialog != null);

			IProjectItem item = project.GetItem(path);

			if (!item.IsVisible)
			{
				if (dialog != null)
					include = PromptToIncludeOutput(dialog, path.RelativeTo(project.Directory), out continueToPrompt);

				if (include)
				{
					includedPath = !item.IsVisible;

					item.Show();

					return continueToPrompt;
				}
			}

			if (item.IsDirectory && item.IsVisible)
				// directories must always be refreshed in case their contents have changed
				item.Refresh();

			return continueToPrompt;
		}

		private bool PromptToIncludeOutput(IncludeOutputMessageBox dialog, string projectItemToInclude, out bool continueToPrompt)
		{
			dialog.BuildItem = projectItemToInclude;

			bool include = dialog.ShowDialog(DocProjectEnvironment.MainWindow) == DialogResult.Yes;

			bool applyToAll = dialog.ApplyToAllChecked;
			bool dontAskAgain = dialog.DontAskMeAgainChecked;

			continueToPrompt = !applyToAll && !dontAskAgain;

			if (dontAskAgain)
			{
				if (Options.DoNotAskIncludeOutputInProject != true)
					// this check prevents a modification to the project file when the property's value is already correct
					Options.DoNotAskIncludeOutputInProject = true;

				if (Options.DefaultIncludeOutputInProject != include)
					// this check prevents a modification to the project file when the property's value is already correct
					Options.DefaultIncludeOutputInProject = include;
			}

			if (!applyToAll && Options.DefaultApplyToAll)
				// this check prevents setting DefaultApplyToAll to false if it's already false so that nothing is written to the project file
				Options.DefaultApplyToAll = false;
			else if (applyToAll && !Options.DefaultApplyToAll)
				// this check prevents setting DefaultApplyToAll to true if it's already true so that nothing is written to the project file
				Options.DefaultApplyToAll = true;

			return include;
		}

		/// <summary>
		/// Releases the unmanaged resources used by an instance of the <see cref="BuildEngine" /> class and optionally releases the managed resources.
		/// </summary>
		/// <param name="disposing"><strong>true</strong> to release both managed and unmanaged resources; <strong>false</strong> to release only unmanaged resources.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		protected override void Dispose(bool disposing)
		{
			try
			{
				if (disposing)
				{
					lock (buildSync)
					{
						Exception lastError = null;

						if (steps != null && steps.Count > 0)
						{
							foreach (IBuildStep step in steps)
							{
								try
								{
									step.Dispose();
								}
								catch (Exception ex)
								{
									lastError = ex;
								}
							}

							steps = null;
						}

						if (lastError != null)
							throw lastError;
					}
				}
			}
			finally
			{
				base.Dispose(disposing);
			}
		}
		#endregion

		#region Events
		private readonly object BuildStartingEvent = new object();
		private readonly object BuildCompletedEvent = new object();

		/// <summary>
		/// Event raised after the engine has completed the build.
		/// </summary>
		[Category("Build")]
		[Description("Event raised after the engine has completed the build.")]
		public event EventHandler<BuildEventArgs> BuildCompleted
		{
			add
			{
				lock (BuildCompletedEvent)
				{
					Events.AddHandler(BuildCompletedEvent, value);
				}
			}
			remove
			{
				lock (BuildCompletedEvent)
				{
					Events.RemoveHandler(BuildCompletedEvent, value);
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="BuildCompleted" /> event.
		/// </summary>
		/// <param name="e"><see cref="BuildEventArgs" /> object that provides the arguments for the event.</param>
		protected virtual void OnBuildCompleted(BuildEventArgs e)
		{
			EventHandler<BuildEventArgs> handler = null;

			lock (BuildCompletedEvent)
			{
				handler = (EventHandler<BuildEventArgs>) Events[BuildCompletedEvent];
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised before the engine will start building.
		/// </summary>
		[Category("Build")]
		[Description("Event raised before the engine will start building.")]
		public event EventHandler<BuildEventArgs> BuildStarting
		{
			add
			{
				lock (BuildStartingEvent)
				{
					Events.AddHandler(BuildStartingEvent, value);
				}
			}
			remove
			{
				lock (BuildStartingEvent)
				{
					Events.RemoveHandler(BuildStartingEvent, value);
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="BuildStarting" /> event.
		/// </summary>
		/// <param name="e"><see cref="BuildEventArgs" /> object that provides the arguments for the event.</param>
		protected virtual void OnBuildStarting(BuildEventArgs e)
		{
			EventHandler<BuildEventArgs> handler = null;

			lock (BuildStartingEvent)
			{
				handler = (EventHandler<BuildEventArgs>) Events[BuildStartingEvent];
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion

		#region Explicit IBuildEngine Members
		BuildSettings IBuildEngine.Settings
		{
			get
			{
				// return Property value for lazy initialization support
				return Settings;
			}
		}

		DocProjectOptions IBuildEngine.Options
		{
			get
			{
				// return Property value for lazy initialization support
				return Options;
			}
		}
		#endregion
	}

	/// <summary>
	/// Provides arguments for <see cref="BuildEngine{TSettings,TOptions}" /> build events.
	/// </summary>
	[Serializable]
	public class BuildEventArgs : EventArgs
	{
		#region Public Properties
		public BuildContext Context
		{
			get
			{
				return context;
			}
		}
		#endregion

		#region Private / Protected
		[NonSerialized]
		private readonly BuildContext context;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="BuildEventArgs" /> class.
		/// </summary>
		public BuildEventArgs(BuildContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			this.context = context;
		}
		#endregion
	}
}
